Skip to content

Fix narrowing for unions#20728

Merged
hauntsaninja merged 2 commits intopython:masterfrom
hauntsaninja:narrow81
Feb 6, 2026
Merged

Fix narrowing for unions#20728
hauntsaninja merged 2 commits intopython:masterfrom
hauntsaninja:narrow81

Conversation

@hauntsaninja
Copy link
Collaborator

@hauntsaninja hauntsaninja commented Feb 3, 2026

Previously we considered the else branch unreachable in the testNarrowingAnyUnion test case. It's also nice that the new code is more obviously correct

Fixes #20330

This will help with landing #20727 as well

@hauntsaninja hauntsaninja changed the title Fix narrowing for unions with Any Fix narrowing for unions Feb 3, 2026
Previously we considered the else branch in the test case here
unreachable
@github-actions

This comment has been minimized.

@hauntsaninja
Copy link
Collaborator Author

mesonbuild is desirable. the line is already type ignored because it is unsafe, now we get a better error message
starlette is a nice improvement, i will add a test for it
homeassistant huawei_lte is great, it makes mypy understand reachable code is reachable
homeassistant entity_registry is unfortunate. we infer dict[literal[...], any] where we used to infer dict[str, any]

[builtins fixtures/tuple.pyi]


[case testTypeIsGeneric]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is #20330 . The regression meant we only accepted negative

if is_async_callable(f):
reveal_type(f) # N: Revealed type is "(def (*Any, **Any) -> typing.Awaitable[Any]) | (def () -> typing.Awaitable[builtins.int])"
else:
reveal_type(f) # N: Revealed type is "def () -> builtins.int | typing.Awaitable[builtins.int]"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is essentially the starlette code. On master the else branch narrowing doesn't happen.

# ...but not regular subtyping relationships
if isinstance(x, FloatLike):
reveal_type(x) # N: Revealed type is "__main__.FloatLike | __main__.IntLike"
reveal_type(x) # N: Revealed type is "__main__.FloatLike"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fully clear what this was testing. The change here is just that we simplify the union. I think the real thing this wants to test is that the else branch is unreachable, so I added that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think the else part is the important one. Not simplifying the union is an implementation detail, the two types are equivalent.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 3, 2026

Diff from mypy_primer, showing the effect of this PR on open source code:

starlette (https://github.com/encode/starlette)
- starlette/middleware/errors.py:178: error: Argument 1 to "run_in_threadpool" has incompatible type "Callable[[Request[State], Exception], Response | Awaitable[Response]] | Callable[[WebSocket, Exception], Awaitable[None]]"; expected "Callable[[Request[State], Exception], Response]"  [arg-type]
+ starlette/middleware/errors.py:178: error: Argument 1 to "run_in_threadpool" has incompatible type "Callable[[Request[State], Exception], Response | Awaitable[Response]]"; expected "Callable[[Request[State], Exception], Response]"  [arg-type]
- starlette/_exception_handler.py:61: error: Argument 1 to "run_in_threadpool" has incompatible type "Callable[[Request[State], Exception], Response | Awaitable[Response]] | Callable[[WebSocket, Exception], Awaitable[None]]"; expected "Callable[[Request[State] | WebSocket, Exception], Any | None]"  [arg-type]
+ starlette/_exception_handler.py:61: error: Argument 2 to "run_in_threadpool" has incompatible type "Request[State] | WebSocket"; expected "Request[State]"  [arg-type]

core (https://github.com/home-assistant/core)
+ homeassistant/components/config/entity_registry.py:231: error: Invalid index type "Literal['aliases']" for "dict[Literal['device_class', 'disabled_by', 'hidden_by', 'icon', 'name', 'new_entity_id', 'area_id'], Any]"; expected type "Literal['device_class', 'disabled_by', 'hidden_by', 'icon', 'name', 'new_entity_id', 'area_id']"  [index]
+ homeassistant/components/config/entity_registry.py:235: error: Invalid index type "Literal['labels']" for "dict[Literal['device_class', 'disabled_by', 'hidden_by', 'icon', 'name', 'new_entity_id', 'area_id'], Any]"; expected type "Literal['device_class', 'disabled_by', 'hidden_by', 'icon', 'name', 'new_entity_id', 'area_id']"  [index]
+ homeassistant/components/config/entity_registry.py:260: error: Invalid index type "Literal['categories']" for "dict[Literal['device_class', 'disabled_by', 'hidden_by', 'icon', 'name', 'new_entity_id', 'area_id'], Any]"; expected type "Literal['device_class', 'disabled_by', 'hidden_by', 'icon', 'name', 'new_entity_id', 'area_id']"  [index]
- homeassistant/components/huawei_lte/__init__.py:192: error: Right operand of "and" is never evaluated  [unreachable]

meson (https://github.com/mesonbuild/meson)
+ mesonbuild/modules/rust.py:189: error: Unused "type: ignore" comment  [unused-ignore]
+ mesonbuild/modules/rust.py:189:39: error: TypedDict "ExecutableKeywordArguments" has no key "prelink"  [typeddict-item]
+ mesonbuild/modules/rust.py:189:39: note: Error code "typeddict-item" not covered by "type: ignore" comment
+ mesonbuild/modules/rust.py:189:39: error: TypedDict "ExecutableKeywordArguments" has no key "rust_abi"  [typeddict-item]
+ mesonbuild/modules/rust.py:189:39: error: TypedDict "ExecutableKeywordArguments" has no key "version"  [typeddict-item]
+ mesonbuild/modules/rust.py:189:39: error: TypedDict "ExecutableKeywordArguments" has no key "soversion"  [typeddict-item]
+ mesonbuild/modules/rust.py:189:39: error: TypedDict "ExecutableKeywordArguments" has no key "darwin_versions"  [typeddict-item]
+ mesonbuild/modules/rust.py:189:39: error: TypedDict "ExecutableKeywordArguments" has no key "shortname"  [typeddict-item]
+ mesonbuild/modules/rust.py:189:39: error: TypedDict "ExecutableKeywordArguments" has no key "pic"  [typeddict-item]

@hauntsaninja
Copy link
Collaborator Author

Planning on merging this soon, since it is a prerequisite for some changes that need to be landed to unblock a release

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG.

@hauntsaninja hauntsaninja merged commit 79e9c78 into python:master Feb 6, 2026
23 checks passed
@hauntsaninja hauntsaninja deleted the narrow81 branch February 6, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[1.18 regression] Negative type narrowing of TypeIs of Union with TypeVar not working

2 participants